#include "OGLScene.h"

// Constructors and Destructors
OGLSchene::OGLSchene()
{
	setDefaults();
}

OGLSchene::OGLSchene(int OpenGLVersionMajor, int OpenGLVersionMinor)
{
	setDefaults();
	this->oglVerMajor = OpenGLVersionMajor;
	this->oglVerMinor = OpenGLVersionMinor;
}

OGLSchene::OGLSchene(int OpenGLVersionMajor, int OpenGLVersionMinor, int fsaaSamples)
{
	setDefaults();
	this->oglVerMajor = OpenGLVersionMajor;
	this->oglVerMinor = OpenGLVersionMinor;
	this->fsaaSamples = fsaaSamples;
}

OGLSchene::~OGLSchene()
{
	glfwTerminate();

}

// Initialization and Object Creation
int OGLSchene::InitializeSchene(
	std::string windowName,
	int windowHeight, 
	int windowWidth)
{
	// Set Window Params
	this->windowName = windowName;
	this->windowHeight = windowHeight;
	this->windowWidth = windowWidth;

	// Initialize GLFW - Window Manager
	if(!glfwInit())
	{
		fprintf(stderr, "[FATAL] Failed to Initialize GLFW...\n");
		return OGL_FATAL;
	}

	int retval = initWindow();
	if(retval != OGL_SUCCESS)
	{
		glfwTerminate();
		return retval;
	}

	// Initialize GLEW
	if(oglVerMajor <= 3)
	{
		glewExperimental = GL_TRUE;	// Otherwise the program segfaults at glGenVertexArrays()
	}
	GLenum glewInitResult = glewInit();
	if(glewInitResult != GLEW_OK)
	{
		fprintf(stderr, "[FATAL] Failed to Initialize GLEW...\n");
		glfwTerminate();
		return OGL_FATAL;
	}

	// Set up OpenGL Optimizations
	glEnable(GL_DEPTH_TEST); // Enable depth test
	glDepthFunc(GL_LESS); // Accept fragment if it closer to the camera than the former one

	// Clean the drawing surface...
	glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
	
	return OGL_SUCCESS;
}

int OGLSchene::InitializeSchene(std::string windowName)
{
	return InitializeSchene(windowName, DEFAULT_WINDOW_HEIGHT, DEFAULT_WINDOW_WIDTH);
}

int OGLSchene::AddObjectBufferData(std::string bufferName, OGLDataObject *bufferObj, int count)
{
	if(bufferObj != NULL)
	{
		if(bufferObjects.find(bufferName) == bufferObjects.end())
		{
			bufferObjects[bufferName] = bufferObj;
			bufferObjectCount[bufferName] = count;
		}
		else
		{
			fprintf(stderr, "[WARNING] New object not added as Buffer Object with \
the name \"%s\" already exists.\n Use OGLSchene::RemoveObjectBuffer() \
to remove it first.\n",
				bufferName.c_str());
			return OGL_WARNING;
		}
	}
	else
	{
		fprintf(stderr,"[ERROR] NULL OGLDataObject* sent to OGLSchene::AddObjectBuffer().\n");
		return OGL_ERROR;
	}

	return OGL_SUCCESS;
}

int OGLSchene::AddVAOID(std::string vaoName, GLuint vaoID)
{	
	if(vaoIDs.find(vaoName) == vaoIDs.end())
		vaoIDs[vaoName] = vaoID;
	else
	{
		fprintf(stderr, "[WARNING] New vaoID not added as a Vertex Array Object ID with \
the name \"%s\" already exists.\n Use OGLSchene::RemoveVAO() to remove it first.\n",
			vaoName.c_str());
		return OGL_WARNING;
	}

	return OGL_SUCCESS;
}

int OGLSchene::AddProgramID(std::string programName, GLuint programID)
{
	if(programIDs.find(programName) == programIDs.end())
		programIDs[programName] = programID;
	else
	{
		fprintf(stderr, "[WARNING] New programID not added as a Program ID with \
the name \"%s\"already exists.\n Use OGLSchene::RemoveProgram() to remove it first.\n",
			programName.c_str());
		return OGL_WARNING;
	}

	return OGL_SUCCESS;
}

int OGLSchene::AddMatrixID(std::string matrixName, GLuint matrixID)
{
	if(matrixIDs.find(matrixName) == matrixIDs.end())
		matrixIDs[matrixName] = matrixID;
	else
	{
		fprintf(stderr, "[WARNING] New matrixID not added as a Matrix ID with \
the name \"%s\"already exists.\n Use OGLSchene::RemoveMatrix() to remove it first.\n",
			matrixName.c_str());
		return OGL_WARNING;
	}

	return OGL_SUCCESS;
}
void OGLSchene::UpdateWindowDims(int windowWidth, int windowHeight, bool forceResize)
{
	this->windowWidth = windowWidth;
	this->windowHeight = windowHeight;
	
	if(forceResize)
		glfwSetWindowSize(windowHeight, windowHeight);
	
	glViewport(0, 0, windowWidth, windowHeight);
}

glm::mat4 OGLSchene::GetPVMat(void)
{
	if(pvMatChange)
	{
		pvMat = projectionMat * viewMat;
		pvMatChange = false;
	}

	return pvMat;
}

int OGLSchene::GetVAOID(std::string vaoName, GLuint *vaoID_out)
{
	std::map<std::string, GLuint>::iterator it = vaoIDs.find(vaoName);
	if(it == vaoIDs.end())
	{
		fprintf(stderr, "[ERROR] No Vertex Array Object ID with the name \"%s\" was found.\n",
			vaoName.c_str());
		return OGL_ERROR;
	}
	else
		*vaoID_out = it->second;

	return OGL_SUCCESS;
}

int OGLSchene::GetProgramID(std::string programName, GLuint *pID_out)
{
	std::map<std::string, GLuint>::iterator it = programIDs.find(programName);
	if(it == programIDs.end())
	{
		fprintf(stderr, "[ERROR] No Program ID with the name \"%s\" was found.\n",
			programName.c_str());
		return OGL_ERROR;
	}
	else
		*pID_out = it->second;
		
	return OGL_SUCCESS;
}

int OGLSchene::GetMatrixID(std::string matrixName, GLuint *mID_out)
{
	std::map<std::string, GLuint>::iterator it = matrixIDs.find(matrixName);
	if(it == matrixIDs.end())
	{
		fprintf(stderr, "[ERROR] No matrix ID with the name \"%s\" was found.\n",
			matrixName.c_str());
		return OGL_ERROR;
	}
	else
		*mID_out = it->second;
		
	return OGL_SUCCESS;
}

int OGLSchene::GetObjectBufferData(std::string bufferName, OGLDataObject **obj_out, int *count_out)
{
	std::map<std::string, OGLDataObject *>::iterator it = bufferObjects.find(bufferName);
	if(it == bufferObjects.end())
	{
		fprintf(stderr, "[ERROR] No OGL Buffer Object with the name \"%s\" was found.\n",
			bufferName.c_str());
		*count_out = 0;
		return OGL_ERROR;
	}
	else
	{
		*obj_out = it->second;
		*count_out = bufferObjectCount[bufferName];
	}	
	return OGL_SUCCESS;
}

std::string OGLSchene::GetWindowName()
{
	return this->windowName;
}

void OGLSchene::GetWindowDims(int *windowWidth_out, int *windowHeight_out)
{
	*windowWidth_out = this->windowWidth;
	*windowHeight_out = this->windowHeight;
}

void OGLSchene::GetCamera(
	glm::vec3 *camLoc_out, 
	glm::vec3 *camDir_out, 
	glm::vec3 *camOrient_out)
{
	*camLoc_out = camLoc;
	*camDir_out = camDir;
	*camOrient_out = camOrient;
}

void OGLSchene::GetScheneParams(
	GLfloat *fov_out,			
	GLfloat *aspectRatio_out,	
	GLfloat *rangeMin_out,		
	GLfloat *rangeMax_out)
{
	*fov_out = fov;
	*aspectRatio_out = aspectRatio;
	*rangeMin_out = rangeMin;
	*rangeMax_out = rangeMax;
}

// Schene Control
int OGLSchene::SetCamera()
{
	SetCamera(defaultCamLoc, defaultCamLoc, defaultCamOrient);

	return OGL_SUCCESS;
}

int OGLSchene::SetCamera(
	glm::vec3 camLoc,	 // position of camera in the world
	glm::vec3 camDir,	 // direction towards which camera is facing
	glm::vec3 camOrient) // orientation of camera lens
{
	viewMat = glm::lookAt(camLoc, camDir, camOrient);
	pvMatChange = true;

	this->camLoc = camLoc;
	this->camDir = camDir;
	this->camOrient = camOrient;

	return OGL_SUCCESS;
}

int OGLSchene::SetScheneParams()
{
	SetScheneParams(
		defaultFov, 
		defaultAspectRatio, 
		defaultRangeMin, 
		defaultRangeMax
	);

	return OGL_SUCCESS;
}

int OGLSchene::SetScheneParams(
	GLfloat fov,			// Field of view
	GLfloat aspectRatio,	// Aspect ratio
	GLfloat rangeMin,		// View range minimum
	GLfloat rangeMax)		// View range maximum
{
	projectionMat = glm::perspective(fov, aspectRatio, rangeMin, rangeMax);
	pvMatChange = true;

	this->fov = fov;
	this->aspectRatio = aspectRatio;
	this->rangeMin = rangeMin;
	this->rangeMax = rangeMax;

	return OGL_SUCCESS;
}

int OGLSchene::RemoveObjectBufferData(std::string bufferName)
{
	std::map<std::string, OGLDataObject *>::iterator it = bufferObjects.find(bufferName);
	int retval = OGL_SUCCESS;
	if(it != bufferObjects.end())
	{
		OGLDataObject *objDat = it->second;
		if(objDat != NULL)
		{
			int count = bufferObjectCount[bufferName];
			DestroyOGLObjects(objDat, count);
			bufferObjectCount.erase(bufferName);
		}
		else
		{
			fprintf(stderr, "[WARNING] OGL Object associated with name \"%s\" is NULL.\n",
				bufferName.c_str());
			retval = OGL_WARNING;
		}
		bufferObjects.erase(it);
	}
	else
	{
		fprintf(stderr,"[ERROR] No OGL Object Buffer with the name \"%s\" exists. \
			Nothing was removed.\n", bufferName.c_str());
		return  OGL_ERROR;
	}

	return retval;
}

int OGLSchene::RemoveVAO(std::string vaoName)
{
	std::map<std::string, GLuint>::iterator it = vaoIDs.find(vaoName);
	if(it != vaoIDs.end())
	{
		vaoIDs.erase(it);
	}
	else
	{
		fprintf(stderr,"[ERROR] No Vertex Array Object ID with the name \"%s\" exists. \
			Nothing was removed.\n", vaoName.c_str());
		return  OGL_ERROR;
	}

	return OGL_SUCCESS;
}

int OGLSchene::RemoveProgram(std::string programName)
{
	std::map<std::string, GLuint>::iterator it = programIDs.find(programName);
	if(it != programIDs.end())
	{
		programIDs.erase(it);
	}
	else
	{
		fprintf(stderr,"[ERROR] No Program ID with the name \"%s\" exists. \
			Nothing was removed.\n", programName.c_str());
		return  OGL_ERROR;
	}

	return OGL_SUCCESS;
}

int OGLSchene::RemoveMatrix(std::string matrixName)
{
	std::map<std::string, GLuint>::iterator it = matrixIDs.find(matrixName);
	if(it != matrixIDs.end())
	{
		matrixIDs.erase(it);
	}
	else
	{
		fprintf(stderr,"[ERROR] No Matrix ID with the name \"%s\" exists. \
			Nothing was removed.\n", matrixName.c_str());
		return  OGL_ERROR;
	}

	return OGL_SUCCESS;
}

int OGLSchene::DestroyScheneData()
{
	destroyVBOs();
	destroyShaders();

	int retval = OGL_SUCCESS;
	// Free OGLObjects
	std::map<std::string, OGLDataObject *>::reverse_iterator rit;
	for(rit = bufferObjects.rbegin(); rit != bufferObjects.rend(); rit++)
	{
		OGLDataObject *objDat = rit->second;
		if(objDat != NULL)
		{
			int count = bufferObjectCount[rit->first];
			DestroyOGLObjects(objDat, count);
			bufferObjectCount.erase(rit->first);
		}
		else
		{
			fprintf(stderr, "[WARNING] OGL Object associated with name \"%s\" is NULL.\n",
				rit->first.c_str());
			retval = OGL_WARNING;
		}
	}
	
	// Clear the Maps
	bufferObjects.clear();
	bufferObjectCount.clear();
	vaoIDs.clear();
	programIDs.clear();

	glfwTerminate();

	return retval;
}

void OGLSchene::setDefaults()
{
	this->defaultCamLoc = glm::vec3(0.0f, 0.0f, 50.0f);	// Top-down view
	this->defaultCamDir = glm::vec3(0.0f, 0.0f, 0.0f);	// Points at origin
	this->defaultCamOrient = glm::vec3(0.0f, 1.0f, 0.0f);	// Oriented right-side-up

	this->defaultFov = 60.0f;
	this->defaultAspectRatio = 16.0f / 9.0f;
	this->defaultRangeMin = 0.001f;
	this->defaultRangeMax = 1000.0f;

	this->oglVerMajor = DEFAULT_OPENGL_VERSION_MAJOR;
	this->oglVerMinor = DEFAULT_OPENGL_VERSION_MINOR;
	this->fsaaSamples = DEFAULT_FSAA_SAMPLES;
	this->pvMat = glm::mat4(1.0);
	this->bufferObjects.clear();
	this->vaoIDs.clear();
	this->programIDs.clear();

	// Set up view and projection matrices with default values
	SetCamera();
	SetScheneParams();
}

int OGLSchene::initWindow()
{
	glfwOpenWindowHint(GLFW_FSAA_SAMPLES, fsaaSamples);	// Sets 4x Anit-Aliazing Samples
	glfwOpenWindowHint(GLFW_OPENGL_VERSION_MAJOR, oglVerMajor);
	glfwOpenWindowHint(GLFW_OPENGL_VERSION_MINOR, oglVerMinor);
	glfwOpenWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);	// We dont' want old OpenGL
	glfwOpenWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);	// Allows forward compatibility
	glfwOpenWindowHint(GLFW_OPENGL_DEBUG_CONTEXT, GL_TRUE);		// Creates an OpenGL Debug Context

	// Create the Window
	if(!glfwOpenWindow(
		this->windowWidth, this->windowHeight, 0, 0, 0, 0, 32, 0, GLFW_WINDOW))
	{
		fprintf(stderr, "[FATAL] Could not create OpenGL Window...\n");
		return OGL_FATAL;
	}

	// Set GLFW parameters
	glfwSetWindowTitle(this->windowName.c_str());	// Set window Title
	glfwEnable(GLFW_SYSTEM_KEYS);	// Enables the use of Alt + Tab, Ctrl + Alt + Del, etc.
//	glfwEnable(GLFW_STICKY_KEYS);	// Enables Key Release only after check using glfwGetKey

	return OGL_SUCCESS;
}

int OGLSchene::destroyVBOs()
{
	GLenum ErrorCheckValue = glGetError();

	// Bind Vertex Array Object
	// TODO : Figure out how to bind the correct VAO for each OGLDataObject buffer
	glBindVertexArray(vaoIDs["VAO"]);

	
	// TODO : Bind the correct vertex attribute arrays for each VBO
	glBindBuffer(GL_ARRAY_BUFFER, 0);
	std::map<std::string, OGLDataObject *>::iterator it;
	for(it = bufferObjects.begin(); it != bufferObjects.end(); it++)
	{
		std::vector<GLuint>::iterator vit;
		// Disable Vertex Attribute Buffers
		for(vit = it->second->vaPointerIndex->begin(); 
			vit != it->second->vaPointerIndex->end(); vit++)
		{
			glDisableVertexAttribArray(*vit);
		}
		// Delete Vertex Buffers
		glDeleteBuffers(1, &(it->second->vboID));
	}

	// Delete Vertex Array Object
	glBindVertexArray(0);	
	glDeleteVertexArrays(1, &(vaoIDs["VAO"]));

	ErrorCheckValue = glGetError();
	if (ErrorCheckValue != GL_NO_ERROR)
	{
		fprintf(
			stderr,
			"[ERROR] Could not destroy the VBO: %s \n",
			gluErrorString(ErrorCheckValue)
		);

		return OGL_ERROR;
	}

	return OGL_SUCCESS;
}

int OGLSchene::destroyShaders()
{
	GLenum ErrorCheckValue = glGetError();

	glUseProgram(0);
	std::map<std::string, GLuint>::iterator it;
	for(it = programIDs.begin(); it != programIDs.end(); it++)
	{
		GLuint programID = it->second;
		GLsizei count = 0;
		GLuint *shaderIDs = (GLuint *)malloc(sizeof(GLuint) * MAX_SHADERS_PER_PROGRAM);
		glGetAttachedShaders(programID, MAX_SHADERS_PER_PROGRAM, &count, shaderIDs);
		for(GLsizei i = 0; i < count; i++)
			glDetachShader(programID, shaderIDs[i]); 
		for(GLsizei i = 0; i < count; i++)
			glDeleteShader(shaderIDs[i]); 
		
		glDeleteProgram(programID);
		free(shaderIDs);
	}
	ErrorCheckValue = glGetError();
	if (ErrorCheckValue != GL_NO_ERROR)
	{
		fprintf(
			stderr,
			"[ERROR] Could not destroy the shaders: %s \n",
			gluErrorString(ErrorCheckValue)
		);

		return OGL_ERROR;
	}

	return OGL_SUCCESS;
}